import path from "path";
import type { AST, Rule } from "eslint";
import picocolors from "picocolors";
import { createSyncFn } from "synckit";
import type { PrettierFormatFn } from "./workers/prettier-format";
import type { ResolvePrettierOptionsFn } from "./workers/resolve-prettier-options";
import type { ResolvePrettierFileInfoFn } from "./workers/resolve-prettier-file-info";
import { showInvisibles, generateDifferences } from "prettier-linter-helpers";

const { INSERT, DELETE, REPLACE } = generateDifferences;

let resolvePrettierOptions: ResolvePrettierOptionsFn | undefined;
let resolvePrettierFileInfo: ResolvePrettierFileInfoFn | undefined;

let prettierFormat: PrettierFormatFn | undefined;

const DEFAULT_VUE_NODE_LANG = {
  template: "html",
  script: "js",
  style: "css",
};

export const rule: Rule.RuleModule = {
  meta: {
    type: "layout",
    docs: {
      description: "Make your code prettier",
      recommended: true,
      url: "https://github.com/meteorlxy/eslint-plugin-prettier-vue#eslint-config",
    },

    fixable: "code",

    messages: {
      [INSERT]: "Insert `{{ insertText }}`",
      [DELETE]: "Delete `{{ deleteText }}`",
      [REPLACE]: "Replace `{{ deleteText }}` with `{{ insertText }}`",
    },
    schema: [
      {
        type: "object",
        properties: {
          template: { type: "boolean", default: true },
          script: { type: "boolean", default: true },
          style: { type: "boolean", default: true },
        },
      },
    ],
  },

  create(context) {
    const rootAST = context.parserServices.getDocumentFragment && context.parserServices.getDocumentFragment();
    if (!rootAST) {
      return {};
    }

    if (!resolvePrettierOptions) {
      resolvePrettierOptions = createSyncFn(require.resolve("./workers/resolve-prettier-options"));
    }
    if (!resolvePrettierFileInfo) {
      resolvePrettierFileInfo = createSyncFn(require.resolve("./workers/resolve-prettier-file-info"));
    }

    const prettierVueSettings = context.options[0] || {};
    // Processors that extract content from a file, such as the markdown
    // plugin extracting fenced code blocks may choose to specify virtual
    // file paths. If this is the case then we need to resolve prettier
    // config and file info using the physical path instead of the virtual
    // path.
    const physicalFilepath = context.physicalFilename ?? context.getPhysicalFilename(); // 文件在磁盘上的路径

    const filepath = context.filename ?? context.getFilename(); // 带路径的文件名
    const sourceCode = context.sourceCode ?? context.getSourceCode(); // 源代码

    if (!prettierFormat) {
      prettierFormat = createSyncFn(require.resolve("./workers/prettier-format"));
    }

    let prettierOptions;
    // 处理 Vue SFC
    if (filepath.endsWith(".vue")) {
      prettierOptions = resolvePrettierOptions(physicalFilepath);
      const { ignored } = resolvePrettierFileInfo(physicalFilepath, prettierOptions.plugins as string[]);

      if (ignored) {
        return {};
      }
    }

    return {
      Program() {
        // 提取 vue 文件所有根节点
        const SFCBlocks = rootAST.children
          .filter(node => node.type === "VElement" && prettierVueSettings[node.name] && DEFAULT_VUE_NODE_LANG[node.name])
          .map(node => ({
            // source: sourceCode.getText(node).slice(node.startTag.range[1], node.endTag.range[0]).replace(/^\n/, ""),
            // offset: node.startTag.range[1] + 1,
            source: sourceCode.getText(node),
            offset: node.range[0],
            lang: node.startTag.attributes.find((attr: any) => attr.key.name === "lang")?.value?.value || DEFAULT_VUE_NODE_LANG[node.name],
            type: node.name,
          }));

        // Run prettier on each of the SFC blocks respectively
        SFCBlocks.forEach(({ source, offset, lang, type }) => {
          // Disguise SFC block as an individual file
          const fakeFilePath = `${filepath}--${type}.${lang}`;
          // const { inferredParser } = resolvePrettierFileInfo(fakeFilePath, prettierOptions.plugins as string[]);

          let prettierSource: string;
          try {
            prettierSource = prettierFormat(source, {
              ...prettierOptions,
              parser: "vue",
              filepath: fakeFilePath,
            }).replace(/\r?\n?$/, "");
          } catch (err) {
            if (!(err instanceof Error)) {
              throw err;
            }

            // UndefinedParserError
            if (err.message.startsWith("No parser could be inferred for file")) {
              console.warn(
                picocolors.yellow("warning"),
                "[prettier-vue]",
                `No parser could be inferred for "${path.extname(fakeFilePath ?? "")}" format`
              );
            }

            if (!(err instanceof SyntaxError)) {
              throw err;
            }

            let message = `Parsing error: ${err.message}`;

            const error = err as unknown as {
              codeFrame: string;
              loc: AST.SourceLocation;
            };
            // Prettier's message contains a codeframe style preview of the
            // invalid code and the line/column at which the error occurred.
            // ESLint shows those pieces of information elsewhere already so
            // remove them from the message
            if (error.codeFrame) {
              message = message.replace(`\n${error.codeFrame}`, "");
            }
            if (error.loc) {
              message = message.replace(/ \(\d+:\d+\)$/, "");
            }

            context.report({ message, loc: error.loc });

            return;
          }

          if (source !== prettierSource) {
            // 获取代码差异，然后报告
            generateDifferences(source, prettierSource).forEach(difference => {
              // reportDifference(context, difference, difference.offset + offset)
              const diffOffset = difference.offset + offset;
              const { operation, deleteText = "", insertText = "" } = difference;
              const range: AST.Range = [diffOffset, diffOffset + deleteText.length];
              const [start, end] = range.map(index => (context.sourceCode ?? context.getSourceCode()).getLocFromIndex(index));

              context.report({
                messageId: operation,
                data: {
                  deleteText: showInvisibles(deleteText),
                  insertText: showInvisibles(insertText),
                },
                loc: { start, end },
                fix: fixer => fixer.replaceTextRange(range, insertText),
              });
            });
          }
        });
      },
    };
  },
};
